{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    ">原文地址 [mp.weixin.qq.com](https://mp.weixin.qq.com/s/3d5IlLcyklVsGDOqeipIYw)\n",
    "\n",
    "> 之前有很多朋友提过，当使用`docker-maven-plugin`打包 SpringBoot 应用的 Docker 镜像时，服务器需要开放`2375`端口。由于开放了端口没有做任何安全保护，会引起安全漏洞，被人入侵、挖矿、CPU 飙升这些情况都有发生，今天我们来聊聊如何解决这个问题。\n",
    "\n",
    "问题产生的原因\n",
    "-------\n",
    "\n",
    "首先我们要明白问题产生的原因，才能更好地解决问题！\n",
    "\n",
    "Docker 为了实现集群管理，提供了远程管理的端口。Docker Daemon 作为守护进程运行在后台，可以执行发送到管理端口上的 Docker 命令。\n",
    "\n",
    "当我们修改`docker.service`文件，修改启动命令，加入`-H tcp://0.0.0.0:2375`时，就会开放`2375`端口，且没有任何加密和认证过程，这种方式一般用在内网测试环境。如果你的服务器部署在公网上，任何知道你 IP 的人，都可以管理这台主机上的容器和镜像，想想就觉得可怕。\n",
    "\n",
    "解决思路\n",
    "----\n",
    "\n",
    "开放远程管理端口后，没有做任何安全保护导致了这个问题。我们只要使用安全传输层协议（TLS）进行传输并使用 CA 认证即可。\n",
    "\n",
    "制作证书及秘钥\n",
    "-------\n",
    "\n",
    "> 我们需要使用 OpenSSL 制作 CA 机构证书、服务端证书和客户端证书，以下操作均在安装 Docker 的 Linux 服务器上进行。\n",
    "\n",
    "*   首先创建一个目录用于存储生成的证书和秘钥；\n",
    "    \n",
    "\n",
    "```\n",
    "mkdir /mydata/docker-ca && cd /mydata/docker-ca\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   创建 CA 证书私钥，期间需要输入两次用户名和密码，生成文件为`ca-key.pem`；\n",
    "    \n",
    "\n",
    "```\n",
    "openssl genrsa -aes256 -out ca-key.pem 4096\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   根据私钥创建 CA 证书，期间需要输入上一步设置的私钥密码，生成文件为`ca.pem`；\n",
    "    \n",
    "\n",
    "```\n",
    "openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -subj \"/CN=\\*\" -out ca.pem\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   创建服务端私钥，生成文件为`server-key.pem`；\n",
    "    \n",
    "\n",
    "```\n",
    "openssl genrsa -out server-key.pem 4096\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   创建服务端证书签名请求文件，用于 CA 证书给服务端证书签名，生成文件`server.csr`；\n",
    "    \n",
    "\n",
    "```\n",
    "openssl req -subj \"/CN=\\*\" -sha256 -new -key server-key.pem -out server.csr\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   创建 CA 证书签名好的服务端证书，期间需要输入 CA 证书私钥密码，生成文件为`server-cert.pem`；\n",
    "    \n",
    "\n",
    "```\n",
    "openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   创建客户端私钥，生成文件为`key.pem`；\n",
    "    \n",
    "\n",
    "```\n",
    "openssl genrsa -out key.pem 4096\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   创建客户端证书签名请求文件，用于 CA 证书给客户证书签名，生成文件`client.csr`；\n",
    "    \n",
    "\n",
    "```\n",
    "openssl req -subj \"/CN=client\" -new -key key.pem -out client.csr\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   为了让秘钥适合客户端认证，创建一个扩展配置文件`extfile-client.cnf`；\n",
    "    \n",
    "\n",
    "```\n",
    "echo extendedKeyUsage = clientAuth > extfile-client.cnf\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   创建 CA 证书签名好的客户端证书，期间需要输入 CA 证书私钥密码，生成文件为`cert.pem`；\n",
    "    \n",
    "\n",
    "```\n",
    "openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem -extfile extfile-client.cnf\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   删除创建过程中多余的文件；\n",
    "    \n",
    "\n",
    "```\n",
    "rm -rf ca.srl server.csr client.csr extfile-client.cnf\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   最终生成文件如下，有了它们我们就可以进行基于 TLS 的安全访问了。\n",
    "    \n",
    "\n",
    "```\n",
    "ca.pem CA证书\n",
    "ca-key.pem CA证书私钥\n",
    "server-cert.pem 服务端证书\n",
    "server-key.pem 服务端证书私钥\n",
    "cert.pem 客户端证书\n",
    "key.pem 客户端证书私钥\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "配置 Docker 支持 TLS\n",
    "----------------\n",
    "\n",
    "*   用 vim 编辑器修改 docker.service 文件；\n",
    "    \n",
    "\n",
    "```\n",
    "vi /usr/lib/systemd/system/docker.service\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   修改以`ExecStart`开头的配置，开启 TLS 认证，并配置好 CA 证书、服务端证书和服务端私钥，修改内容如下；\n",
    "    \n",
    "\n",
    "```\n",
    "ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375 --tlsverify --tlscacert=/mydata/docker-ca/ca.pem --tlscert=/mydata/docker-ca/server-cert.pem --tlskey=/mydata/docker-ca/server-key.pem\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   重启 Docker 服务，这样我们的 Docker 服务就支持使用 TLS 进行远程访问了！\n",
    "    \n",
    "\n",
    "```\n",
    "systemctl daemon-reload && systemctl restart docker\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "客户端访问\n",
    "-----\n",
    "\n",
    "> 接下来我们将使用`docker-maven-plugin`来打包 Docker 镜像，使用的代码为原来的`mall-tiny-docker`例子。\n",
    "\n",
    "*   直接使用`docker-maven-plugin`打包试试，由于我们的插件版本有点低，使用新一点版本的 Docker 会出现如下问题，升级到`1.2.2`版本解决该问题；\n",
    "    \n",
    "\n",
    "```\n",
    "\\[ERROR\\] Failed to execute goal com.spotify:docker-maven-plugin:1.1.0:build (build-image) on project mall-tiny-docker: Exception caught: com.spotify.docker.client.shaded.com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of \\`com.spotify.docker.client.messages.RegistryAuth\\` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('desktop')\n",
    "\\[ERROR\\] at \\[Source: UNKNOWN; line: -1, column: -1\\] (through reference chain: java.util.LinkedHashMap\\[\"credsStore\"\\])\n",
    "\\[ERROR\\] -> \\[Help 1\\]\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   修改完版本后打包，发现 TLS 不再支持`http`了，需要改用`https`，修改`<dockerHost>`配置为`https`；\n",
    "    \n",
    "\n",
    "```\n",
    "\\[ERROR\\] Failed to execute goal com.spotify:docker-maven-plugin:1.2.2:build (build-image) on project mall-tiny-docker: Exception caught: Request error: GET http://192.168.3.101:2375/version: 400, body: Client sent an HTTP request to an HTTPS server. HTTP 400 Bad Request -> \\[Help 1\\]\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   修改完成后再次打包，继续失败，需要添加对应的客户端证书才能访问；\n",
    "    \n",
    "\n",
    "```\n",
    "\\[ERROR\\] Failed to execute goal com.spotify:docker-maven-plugin:1.2.2:build (build-image) on project mall-tiny-docker: Exception caught: java.util.concurrent.ExecutionException: com.spotify.docker.client.shaded.javax.ws.rs.ProcessingException: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target -> \\[Help 1\\]\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   将如下文件复制到指定目录，这里复制到了`I:\\developer\\env\\docker-ca`；\n",
    "    \n",
    "\n",
    "```\n",
    "ca.pem CA证书\n",
    "cert.pem 客户端证书\n",
    "key.pem 客户端证书私钥\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   然后将该目录配置在插件的`<dockerCertPath>`节点下，最终插件配置如下；\n",
    "    \n",
    "\n",
    "```xml\n",
    "<plugin>\n",
    "    <groupId>com.spotify</groupId>\n",
    "    <artifactId>docker-maven-plugin</artifactId>\n",
    "    <version>1.2.2</version>\n",
    "    <executions>\n",
    "        <execution>\n",
    "            <id>build-image</id>\n",
    "            <phase>package</phase>\n",
    "            <goals>\n",
    "                <goal>build</goal>\n",
    "            </goals>\n",
    "        </execution>\n",
    "    </executions>\n",
    "    <configuration>\n",
    "        <imageName>mall-tiny/${project.artifactId}:${project.version}</imageName>\n",
    "        <dockerHost>https://192.168.3.101:2375</dockerHost>\n",
    "        <baseImage>java:8</baseImage>\n",
    "        <entryPoint>\\[\"java\", \"-jar\",\"/${project.build.finalName}.jar\"\\]\n",
    "        </entryPoint>\n",
    "        <dockerCertPath>I:\\\\developer\\\\env\\\\docker-ca</dockerCertPath>\n",
    "        <resources>\n",
    "            <resource>\n",
    "                <targetPath>/</targetPath>\n",
    "                <directory>${project.build.directory}</directory>\n",
    "                <include>${project.build.finalName}.jar</include>\n",
    "            </resource>\n",
    "        </resources>\n",
    "    </configuration>\n",
    "</plugin>\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "*   再次打包镜像，发现已经可以成功打包镜像，从此我们的`2375`端口终于可以安全使用了！\n",
    "    \n",
    "\n",
    "```\n",
    "\\[INFO\\] Building image mall-tiny/mall-tiny-docker:0.0.1-SNAPSHOT\n",
    "Step 1/3 : FROM java:8\n",
    "\n",
    " ---> d23bdf5b1b1b\n",
    "Step 2/3 : ADD /mall-tiny-docker-0.0.1-SNAPSHOT.jar //\n",
    "\n",
    " ---> 5cb5a64ccedd\n",
    "Step 3/3 : ENTRYPOINT \\[\"java\", \"-jar\",\"/mall-tiny-docker-0.0.1-SNAPSHOT.jar\"\\]\n",
    "\n",
    " ---> Running in 5f3ceefdd974\n",
    "Removing intermediate container 5f3ceefdd974\n",
    " ---> ee9d0e2b0114\n",
    "ProgressMessage{id=null, status=null, stream=null, error=null, progress=null, progressDetail=null}\n",
    "Successfully built ee9d0e2b0114\n",
    "Successfully tagged mall-tiny/mall-tiny-docker:0.0.1-SNAPSHOT\n",
    "\\[INFO\\] Built mall-tiny/mall-tiny-docker:0.0.1-SNAPSHOT\n",
    "\\[INFO\\] ------------------------------------------------------------------------\n",
    "\\[INFO\\] BUILD SUCCESS\n",
    "\\[INFO\\] ------------------------------------------------------------------------\n",
    "\\[INFO\\] Total time: 20.550 s\n",
    "\\[INFO\\] Finished at: 2020-07-31T15:02:15+08:00\n",
    "\\[INFO\\] Final Memory: 50M/490M\n",
    "\\[INFO\\] ------------------------------------------------------------------------\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "参考资料\n",
    "----\n",
    "\n",
    "官方文档：https://docs.docker.com/engine/security/https/\n",
    "\n",
    "项目源码地址\n",
    "------\n",
    "\n",
    "https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-docker\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.7"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {},
   "toc_section_display": true,
   "toc_window_display": false
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
